import Context from '../context';
import { Method } from '../exchange';
import { Filter, FilterChain } from '../utils/filter';

function checkPath(path: string): void {
  if (!path) throw new Error('empty path');
  if (!path.startsWith('/')) throw new Error('path should start with "/"');
}

function checkPrefix(prefix?: string | null): void {
  if (!prefix) return;

  if (!prefix.startsWith('/')) throw new Error('prefix should start with "/"');
  if (prefix.endsWith('/')) throw new Error('prefix cannot end with "/"');
}

/**
 * A normalized path is which not ending with `/`.
 *
 * A path and its normalized path are considered to be equivalent.
 * For example, `/user/login/` is equivalent to `/user/login`.
 *
 * @param path - The path to normalize
 * @returns The normalized path
 */
function normalizePath(path: string): string {
  return path.length == 1 ? path : path.endsWith('/') ? path.substring(0, path.length - 1) : path;
}

export type Handler = {
  filters: Filter<Context>[];
};

export type PathHandlers = {
  [method in Method]?: Handler;
};

export type RouterConfig = {
  prefix?: string | null;
};

// TODO: 支持 path variable
export class Router {
  #prefix: string;
  #mapping: Map<string, PathHandlers>;
  #filters: Filter<Context>[];

  constructor(conf?: RouterConfig) {
    const { prefix } = conf || {};
    checkPrefix(prefix);
    this.#prefix = prefix || '';
    this.#mapping = new Map();
    this.#filters = [];
  }

  get(path: string, ...filters: Filter<Context>[]): Router {
    return this.path('GET', path, ...filters);
  }

  post(path: string, ...filters: Filter<Context>[]): Router {
    return this.path('POST', path, ...filters);
  }

  put(path: string, ...filters: Filter<Context>[]): Router {
    return this.path('PUT', path, ...filters);
  }

  delete(path: string, ...filters: Filter<Context>[]): Router {
    return this.path('DELETE', path, ...filters);
  }

  head(path: string, ...filters: Filter<Context>[]): Router {
    return this.path('HEAD', path, ...filters);
  }

  patch(path: string, ...filters: Filter<Context>[]): Router {
    return this.path('PATCH', path, ...filters);
  }

  connect(path: string, ...filters: Filter<Context>[]): Router {
    return this.path('CONNECT', path, ...filters);
  }

  options(path: string, ...filters: Filter<Context>[]): Router {
    return this.path('OPTIONS', path, ...filters);
  }

  trace(path: string, ...filters: Filter<Context>[]): Router {
    return this.path('TRACE', path, ...filters);
  }

  path(method: Method, path: string, ...filters: Filter<Context>[]): Router {
    checkPath(path);
    if (filters.length > 0) {
      const PATH = normalizePath(this.prefix + path);
      const rhs = this.#mapping.get(PATH) || {};
      rhs[method] = { filters: [...this.#filters, ...filters] };
      this.#mapping.set(PATH, rhs);
    }
    return this;
  }

  get prefix(): string {
    return this.#prefix;
  }

  get mapping(): Map<string, PathHandlers> {
    return this.#mapping;
  }

  nest(path: string, subRouter: Router): Router {
    if (!path) throw new Error('empty path');
    if (!path.startsWith('/')) throw new Error('path should start with "/"');
    if (path.endsWith('/')) throw new Error('path cannot end with "/"');

    if (!subRouter) return this;

    const subMapping = subRouter.mapping;
    subMapping.forEach((pathHandlers, subpath) => {
      const PATH = normalizePath(this.prefix + path + subpath);
      for (const method in pathHandlers) {
        const handler = pathHandlers[method as Method];
        if (handler) {
          handler.filters = [...this.#filters, ...handler.filters];
        }
      }
      this.#mapping.set(PATH, pathHandlers);
    });
    return this;
  }

  use(...filters: Filter<Context>[]): void {
    this.#filters.push(...filters);
  }

  build(): Filter<Context> {
    return async (ctx: Context, chain: FilterChain<Context>): Promise<void> => {
      // TODO: do something
    };
  }
}
